# -*- coding: utf-8 -*-
# Copyright 2011-2012 MagiCycles.com
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


import logging

from google.appengine.ext import db
from google.appengine.api import datastore
#from google.appengine.ext import search
from lib.becer import search

from lib.geomodel import GeoModel
import lib.geomodel.geocell as geocell
import lib.geomodel.geomath as geomath

from common import *

NOT_ORDERABLE_TYPES = [db.BlobProperty,
                       db.TextProperty,
                       ]

def is_orderable(model, order):
    if order.startswith('-'):
        order = order[1:]
    if not hasattr(model, order):
        return False
    attr = getattr(model, order)
    if not isinstance(attr, db.Property):
        return False
    if type(attr) in NOT_ORDERABLE_TYPES:
        return False

    return True

_db_get_cache = {}

def get(keys, **kwargs):
    keys, multiple = datastore.NormalizeAndTypeCheckKeys(keys)
    keys_need_db = [key for key in keys if key not in _db_get_cache]
    if keys_need_db:
        getted = db.get(keys_need_db, **kwargs)
        if len(getted) == 1 and getted[0] == None and not multiple:
            return
        _db_get_cache.update(dict([(x.key(), x) for x in getted if x is not None]))
    ret = [_db_get_cache.get(k, None) for k in keys]
    if multiple:
        return ret
    if len(ret) > 0:
        return ret[0]


def rm(keys):
    keys, _ = datastore.NormalizeAndTypeCheckKeys(keys)
    return [_db_get_cache.pop(k) for k in keys if k in _db_get_cache]


class DBParent(db.Model):
    pass

BASEMODEL_PARENT = DBParent.get_or_insert("DBParent_basemodel_parent")

class BaseModel(db.Model):#polymodel.PolyModel):
    key_template = ""
    db_parent = BASEMODEL_PARENT
    deleted = db.BooleanProperty(default=False)

    @property
    def id(self):
        return self.key().id()

    @property
    def key_name(self):
        return self.key().name()

    @property
    def id_or_name(self):
        return self.key().id_or_name()

    def put(self):
        count = 0
        while count < 3:
            try:
                ret = db.Model.put(self)
                if ret:
                    break
            except db.Timeout:
                count += 1
        else:
            raise db.Timeout()
        rm(self.key())
        return ret

    save = put
    Save = put

    def delete(self):
        rm(self.key())
        return super(BaseModel, self).delete()

    @classmethod
    def gen_key_name(cls, **kw):
        try:
            return cls.key_template % kw
        except KeyError:
            logging.warn('generate key_name failed: %s <- %s',
                cls.key_template, kw)

    @classmethod
    def get_by_key_name(cls, key_names, parent=None, **kwargs):
        try:
            if not parent:
                parent = cls.db_parent
            parent = db._coerce_to_key(parent)
        except db.BadKeyError, e:
            raise db.BadArgumentError(str(e))
        rpc = datastore.GetRpcFromKwargs(kwargs)
        key_names, multiple = datastore.NormalizeAndTypeCheck(key_names, basestring)
        keys = [datastore.Key.from_path(cls.kind(), name, parent=parent)
                for name in key_names]
        if multiple:
            return get(keys, rpc=rpc)
        else:
            return get(keys[0], rpc=rpc)

    @classmethod
    def get_by_id(cls, ids, parent=None, **kwargs):
        if not parent:
            parent = cls.db_parent
        rpc = datastore.GetRpcFromKwargs(kwargs)
        if isinstance(parent, db.Model):
            parent = parent.key()
        ids, multiple = datastore.NormalizeAndTypeCheck(ids, (int, long))
        keys = [datastore.Key.from_path(cls.kind(), id, parent=parent)
                for id in ids]
        if multiple:
            return get(keys, rpc=rpc)
        else:
            return get(keys[0], rpc=rpc)

    @classmethod
    def get_by_id_or_name(cls, ids_or_names, parent=None, **kwargs):
        if not ids_or_names:
            return None
        temp = ids_or_names
        if not isinstance(temp, (list, tuple)):
            temp = (temp,)
        if isinstance(temp[0], (int,long)):
            return cls.get_by_id(ids_or_names, parent=parent, **kwargs)
        else:
            return cls.get_by_key_name(ids_or_names, parent=parent, **kwargs)


class DBLocationModel(GeoModel):
    #location = db.GeoPtProperty()    #defined in GeoModel
    city = db.StringProperty(default="")
    state = db.StringProperty(default="")
    country = db.StringProperty(default="")

    CELL_STORE_RESOLUTION = 3
    ENABLE_CELL_STORE = False

    @PROPERTY
    def location():
        def fget(self):
            return self.location_raw
        def fset(self, loc):
            self.location_raw = loc
            self.save()
            self.update_location()
            if self.ENABLE_CELL_STORE:
                self.add_2_cell_store(loc) #call this before self.location_raw changed
        return locals()

    def add_2_cell_store(self, new_loc):
        if self.location_raw is not None:
            cell_old = geocell.compute(self.location_raw, resolution=self.CELL_STORE_RESOLUTION)
            DBGeoCellStore.remove_entity(cell_old, self)

        cell_new = geocell.compute(new_loc, resolution=self.CELL_STORE_RESOLUTION)
        DBGeoCellStore.add_entity(cell_new, self)

    @staticmethod
    def search_cost_function(num_cells, resolution):
        return 1e10000 if num_cells > pow(geocell._GEOCELL_GRID_SIZE, 2) else 0

    @staticmethod
    def sort(entities, order):
        if order and entities:
            reverse = order.startswith("-")
            order = order[1:] if reverse else order
            def sort_key(entity):
                return getattr(entity, order)
            entities.sort(key=sort_key, reverse=reverse)
        return entities

    @classmethod
    def bounding_box_count(cls, bbox):
        cell_ne = geocell.compute(bbox.north_east, resolution=cls.CELL_STORE_RESOLUTION)
        cell_sw = geocell.compute(bbox.south_west, resolution=cls.CELL_STORE_RESOLUTION)
        cell_set = sorted(geocell.interpolate(cell_ne, cell_sw))

        return DBGeoCellStore.get_cells_contain_count(cell_set, cls)

    @classmethod
    def bounding_box_search(cls, bbox, max_results=1000, order=None):
        max_results = long(max_results)
        cell_ne = geocell.compute(bbox.north_east, resolution=cls.CELL_STORE_RESOLUTION)
        cell_sw = geocell.compute(bbox.south_west, resolution=cls.CELL_STORE_RESOLUTION)

        num_cells = geocell.interpolation_count(cell_ne, cell_sw)
        logging.info("num_cells:%s"%num_cells)
        cell_set = sorted(geocell.interpolate(cell_ne, cell_sw))

        entities = DBGeoCellStore.get_entities(cell_set, cls, max_results=max_results*1.1)
        DBLocationModel.sort(entities, order)
        results = []
        for entity in entities:
            if len(results) >= max_results:
                break
            if (bbox.south <= entity.location.lat <= bbox.north and
                bbox.west <= entity.location.lon <= bbox.east):
                results.append(entity)

        return results

    @classmethod
    def proximity_search(cls, center, max_results=500, max_distance=20000, order=None):
        box = geomath.box_around(center, max_distance)
        entities = cls.bounding_box_search(box, max_results=max_results, order=order)
        results = []
        for entity in entities:
            if len(results) >= max_results:
                break
            if geomath.distance(center, entity.location) <= max_distance:
                results.append(entity)
        return results

    @classmethod
    def proximity_count(cls, center, max_distance=20000):
        box = geomath.box_around(center, max_distance)
        return  cls.bounding_box_count(box)


class DBGeoCellStore(BaseModel):
    key_template = "dbgeocellstore_cell:%(cell)s|model_name:%(model_name)s"
    ids = db.ListProperty(long)
    key_names = db.StringListProperty()

    @classmethod
    def get_or_create(cls, cell, model_name):
        key_name = cls.gen_key_name(cell=cell, model_name=model_name)
        return cls.get_or_insert(key_name=key_name, parent=cls.db_parent)

    @classmethod
    def get_by_cell_and_model_name(cls, cell, model_name):
        key_name = cls.gen_key_name(cell=cell, model_name=model_name)
        return cls.get_by_key_name(key_name)

    @classmethod
    def get_cells_contain_count(cls, cells, dbmodel):
        if type(cells) not in (tuple, list):
            cells = (cells,)
        store_keynames = []
        for cell in cells:
            store_keynames.append(cls.gen_key_name(cell=cell, model_name=dbmodel.__name__))
        stores = cls.get_by_key_name(store_keynames) or []
        count = 0
        for store in stores:
            if store is not None:
                count += len(store.key_names) + len(store.ids)
        return count

    @classmethod
    def get_entities(cls, cells, dbmodel, max_results=1000):
        max_results = long(max_results)
        if type(cells) not in (tuple, list):
            cells = (cells,)

        store_keynames = []
        for cell in cells:
            store_keynames.append(cls.gen_key_name(cell=cell, model_name=dbmodel.__name__))
        stores = cls.get_by_key_name(store_keynames) or []

        model_key_names = []
        model_ids = []
        for store in stores:
            if store is not None:
                model_key_names += store.key_names
                model_ids += store.ids
                if len(model_key_names) + len(model_ids) >= max_results:
                    break

        results = []
        if model_ids:
            results += [m for m in (dbmodel.get_by_id(model_ids[:max_results]) or []) if m is not None]
            if len(results) >= max_results:
                return results
        if model_key_names:
            for m in (dbmodel.get_by_key_name(model_key_names[:max_results]) or []):
                if m is not None:
                    results.append(m)
                if len(results) >= max_results:
                    return results
        return results

    @classmethod
    def set_entities(cls, cell, model_name, ids=None, key_names=None):
        store = cls.get_or_create(cell, model_name)
        if ids is not None:
            store.ids = ids
        if key_names is not None:
            store.key_names = key_names
        store.save()

    @classmethod
    def add_entity(cls, cell, entity):
        model_name = entity.__class__.__name__
        store = cls.get_or_create(cell, model_name)
        if entity.id is not None:
            store.ids.append(entity.id)
        elif entity.key_name is not None:
            store.key_names.append(entity.key_name)
        else:
            raise Exception("no id or names")
        store.save()
        return store

    @classmethod
    def remove_entity(cls, cell, entity):
        model_name = entity.__class__.__name__
        store = cls.get_by_cell_and_model_name(cell, model_name)
        if store is None:
            return None
        if entity.id in store.ids:
            store.ids.remove(entity.id)
        elif entity.key_name in store.key_names:
            store.key_names.remove(entity.key_name)
        else:
            logging.exception("no id or names")
        store.save()
        return store


class DBUser(BaseModel, DBLocationModel, search.SearchableModel):
    @classmethod
    def SearchableProperties(cls):
        return [['name', 'intro','city', 'state', 'country']]

    ENABLE_CELL_STORE = True

    #location = db.GeoPtProperty()    #defined in GeoModel
    uid = db.StringProperty(required=True)
    name = db.StringProperty(required=True)
    email = db.StringProperty(default="")
    photourl = db.StringProperty(default="")
    password = db.StringProperty(default="")
    join_datetime = db.DateTimeProperty(auto_now_add=True)
    last_login_datetime = db.DateTimeProperty(auto_now=True)


    active = db.BooleanProperty(default=True)
    intro = db.StringProperty(default="", multiline=True)
    timezone = db.FloatProperty(default=8.0)
    language = db.StringProperty(default=LANGUAGE[0], choices=LANGUAGE)
    units = db.StringProperty(default=UNITS_DISTANCE[0], choices=UNITS_DISTANCE)
    grade = db.StringProperty(default=MEMBER_TYPE[0], choices=MEMBER_TYPE)


    bikes = db.ListProperty(long, default=[])  # id list of DBBike
    rides = db.ListProperty(long, default=[]) # id list of DBRide
    favorite_routes = db.ListProperty(long, default=[]) # id list of DBRoute

    friends = db.StringListProperty(default=[]) # keyname list of DBUser
    friendrequestlist = db.StringListProperty(default=[])  # keyname list of DBUser
    teams = db.StringListProperty(default=[])  # keyname list of DBTeam

    outsiteusers = db.StringListProperty(default=[])  # keyname list of DBOutSiteUser

    key_template = "dbuser_uid:%(uid)s"

    @classmethod
    def check_dbuser_exist(cls, uid):
        key_name = cls.gen_key_name(uid=uid)
        return cls.get_by_key_name(key_name, parent=cls.db_parent)

    @classmethod
    def get_or_create(cls, uid, name, **kwds):
        dbuser = cls.check_dbuser_exist(uid)
        if not dbuser:
            key_name = cls.gen_key_name(uid=uid)
            dbuser = cls(parent=cls.db_parent, key_name=key_name, uid=uid, name=name, **kwds)
            dbuser.put()
        return dbuser

    @classmethod
    def new_dbuser(cls, uid, name, **kwds):
        key_name = cls.gen_key_name(uid=uid)
        dbuser = cls(parent=cls.db_parent, key_name=key_name, uid=uid, name=name, **kwds)
        dbuser.put()
        return dbuser

    @classmethod
    def new_dbuser_with_outsiteuser(cls, dboutsiteuser, uid, name, **kwds):
        dbuser = cls.new_dbuser(uid, name, **kwds)
        cls.bind_outsite_user(dbuser, dboutsiteuser)
        return dbuser

    @classmethod
    def get_by_outsiteuser(cls, dboutsiteuser):
        if dboutsiteuser.site_user:
            return cls.get_by_key_name(dboutsiteuser.site_user.key_name)
        return None

    @classmethod
    def new_friend_request(cls, me_uid, friend_uid):
        # add me to friend's friend request list
        def txn():
            me = cls.check_dbuser_exist(me_uid)
            friend = cls.check_dbuser_exist(friend_uid)
            if me and friend:
                key_name = me.key_name
                if key_name in friend.friendrequestlist:
                    return
                if key_name in friend.friends:
                    return
                friend.friendrequestlist.append(key_name)
                friend.save()
            return me, friend

        return db.run_in_transaction(txn)

    @classmethod
    def confirm_friend_request(cls, me_uid, friend_uid, agree=True):
        # confirm a friend in my friend request list
        def txn():
            me = cls.check_dbuser_exist(me_uid)
            friend = cls.check_dbuser_exist(friend_uid)
            if me and friend:
                my_key_name = me.key_name
                friend_key_name = friend.key_name
                if friend_key_name in me.friendrequestlist:
                    me.friendrequestlist.remove(friend_key_name)
                    if my_key_name in friend.friendrequestlist:
                        friend.friendrequestlist.remove(my_key_name)

                    if agree:
                        if friend_key_name not in me.friends:
                            me.friends.append(friend_key_name)
                        if my_key_name not in friend.friends:
                            friend.friends.append(my_key_name)
                    me.save()
                    friend.save()
            return me, friend

        return db.run_in_transaction(txn)

    @classmethod
    def remove_a_friendship(cls, me_uid, friend_uid):
        def txn():
            me = cls.check_dbuser_exist(me_uid)
            friend = cls.check_dbuser_exist(friend_uid)
            if me and friend:
                my_key_name = me.key_name
                friend_key_name = friend.key_name
                if friend_key_name in me.friends:
                    me.friends.remove(friend_key_name)
                if my_key_name in friend.friends:
                    friend.friends.remove(my_key_name)
                me.save()
                friend.save()
            return me, friend

        return db.run_in_transaction(txn)

    @classmethod
    def bind_outsite_user(cls, dbuser, dboutsiteuser):
        def txn():
            if dboutsiteuser.site_user:
                raise UserExistError(
                    "%s already bound to %s, pls unbind first" % (dboutsiteuser.uid, dboutsiteuser.site_user.uid))
            for key_name in dbuser.outsiteusers:
                outuser = DBOutSiteUser.get_by_key_name(key_name, parent=cls.db_parent)
                if outuser.site == dboutsiteuser.site:
                    raise UserExistError("Already bind Site %s with user %s" % (outuser.site, outuser.uid))

            dboutsiteuser.site_user = dbuser
            dbuser.outsiteusers.append(dboutsiteuser.key_name)
            dbuser.Save()
            dboutsiteuser.Save()
            return True

        return db.run_in_transaction(txn)

    @classmethod
    def unbind_outsite_user(cls, dbuser, dboutsiteuser):
        def txn():
            if len(dbuser.outsiteusers) <= 1: #only one outsite user bound
                raise UserNotExistError("Can not unbind the last outsite user")
            if dboutsiteuser.key_name in dbuser.outsiteusers:
                dbuser.outsiteusers.remove(dboutsiteuser.key_name)
                dbuser.Save()
            if dboutsiteuser.site_user and dboutsiteuser.site_user.uid == dbuser.uid:
                dboutsiteuser.site_user = None
                dboutsiteuser.Save()

        return db.run_in_transaction(txn)


class DBOutSiteUser(BaseModel):
    site = db.StringProperty(choices=OutSitesTypes)
    name = db.StringProperty()
    uid = db.StringProperty(required=True)
    is_tweetable = db.BooleanProperty(default=False)
    enable_twitter = db.BooleanProperty(default=False)
    site_user = db.ReferenceProperty(reference_class=DBUser, collection_name="outusers")
    access_token_key = db.StringProperty()
    access_token_secret = db.StringProperty()
    expired_time = db.StringProperty(default="0")
    email = db.StringProperty(default="")
    photourl = db.StringProperty(default="")

    key_template = "dboutsiteuser_uid:%(uid)s"

    @classmethod
    def check_dboutsiteuser_exist(cls, uid):
        key_name = cls.gen_key_name(uid=uid)
        return cls.get_by_key_name(key_name, parent=cls.db_parent)

    @classmethod
    def create(cls, uid, name, **kwds):
        key_name = cls.gen_key_name(uid=uid)
        dboutsiteuser = cls(parent=cls.db_parent, key_name=key_name, uid=uid, name=name, **kwds)
        dboutsiteuser.put()
        return dboutsiteuser

    @classmethod
    def get_or_create(cls, uid, **kwds):
        key_name = cls.gen_key_name(uid=uid)
        return cls.get_or_insert(parent=cls.db_parent, key_name=key_name, uid=uid, **kwds)


class DBBike(BaseModel, search.SearchableModel):
    @classmethod
    def SearchableProperties(cls):
        return [search.ALL_PROPERTIES]
    type = db.StringProperty(default=BIKE_TYPE[-1], choices=BIKE_TYPE)
    make = db.StringProperty(default="DIY")
    model = db.StringProperty()
    year = db.StringProperty()
    owner = db.ReferenceProperty(DBUser, collection_name="my_bikes")
    photourl = db.StringProperty(default="")
    create_datetime = db.DateTimeProperty(auto_now_add=True)

    frame = db.StringProperty(default="")
    fork = db.StringProperty(default="")
    wheels = db.StringProperty(default="")
    components = db.StringProperty(default="")
    description = db.StringProperty(default="", multiline=True)

    @property
    def name(self):
        return "%s %s %s" % (self.year, self.make, self.model)

    @classmethod
    def new_bike(cls, type, make, model, year, owner, photourl,
                 frame="", fork="", wheels="", components="", description=""):
        def txn():
            bike = cls(parent=cls.db_parent, type=type, make=make, model=model, year=year, owner=owner,
                photourl=photourl,
                frame=frame, fork=fork, wheels=wheels, components=components, description=description)
            bike.put()
            return bike

        bike = db.run_in_transaction(txn)
        owner.bikes.append(bike.id)
        owner.save()
        return bike

    @classmethod
    def remove_dbbike(cls, dbbike):
        def txn():
            bike_id = dbbike.id
            if bike_id in dbbike.owner.bikes:
                dbbike.owner.bikes.remove(bike_id)
                dbbike.owner.save()
            dbbike.deleted = True
            dbbike.save()
            return dbbike

        return db.run_in_transaction(txn)


class DBDirections(BaseModel):
    direction_blod = db.TextProperty()  #store serialized str of google map DirectionsResult


class DBRoute(BaseModel, DBLocationModel, search.SearchableModel):
    ENABLE_CELL_STORE = True

    @classmethod
    def SearchableProperties(cls):
        return [['name', 'start_address', 'end_address','city', 'state', 'country']]
    #location = db.GeoPtProperty()    #defined in GeoModel
    name = db.StringProperty()
    start_address = db.StringProperty()
    end_address = db.StringProperty()
    start_location = db.GeoPtProperty()
    end_location = db.GeoPtProperty()
    distance = db.IntegerProperty()
    create_datetime = db.DateTimeProperty(auto_now_add=True)

    creator = db.ReferenceProperty(DBUser, collection_name="my_routes")
    directions_store = db.ReferenceProperty(DBDirections)
    preview_url = db.TextProperty(default="")

    route_type = db.StringProperty(default="gmap", choices=ROUTE_TYPE)
    file_url = db.TextProperty(default="")

    @PROPERTY
    def directions():
        def fget(self):
            return self.directions_store.direction_blod

        def fset(self, directions):
            if (self.directions_store == None):
                dbdirections = DBDirections(direction_blod=directions, parent=self.db_parent)
                dbdirections.put()
                self.directions_store = dbdirections
            else:
                self.directions_store.direction_blod = directions

        return locals()

    @classmethod
    def generate_dbroute(cls, name, start_addr, end_addr, start_loc, end_loc,
                         distance, creator, direction_result=None, preview_url="", **kwds):
        route = cls(parent=cls.db_parent, **kwds)
        route.name = name
        route.start_address = start_addr
        route.end_address = end_addr
        route.location = start_loc
        route.start_location = start_loc
        route.end_location = end_loc
        route.distance = distance
        route.creator = creator
        route.directions = direction_result
        route.preview_url = preview_url
        route.put()

        return route


class DBTeam(BaseModel, DBLocationModel, search.SearchableModel):
    ENABLE_CELL_STORE = True
    @classmethod
    def SearchableProperties(cls):
        return [['name', 'intro', 'city', 'state', 'country']]
    #location = db.GeoPtProperty()    #defined in GeoModel
    name = db.StringProperty()
    intro = db.StringProperty(default="", multiline=True)
    create_datetime = db.DateTimeProperty(auto_now_add=True)
    actived = db.BooleanProperty(default=True)
    photourl = db.StringProperty(default="")

    key_template = "dbteam_name:%(name)s|city:%(city)s|state:%(state)s|country:%(country)s"


class DBRide(BaseModel, search.SearchableModel):
    @classmethod
    def SearchableProperties(cls):
        return [['name', 'city', 'state', 'country']]
    bike = db.ReferenceProperty(DBBike, collection_name="bike_rides")
    rider = db.ReferenceProperty(DBUser, collection_name="my_rides")
    route = db.ReferenceProperty(DBRoute, collection_name="route_rides")
    name = db.StringProperty(default="")
    create_datetime = db.DateTimeProperty(auto_now_add=True)
    update_datetime = db.DateTimeProperty(auto_now=True)
    start_datetime = db.DateTimeProperty()
    end_datetime = db.DateTimeProperty()
    duration = db.FloatProperty(default=0) # in hours
    weather = db.StringProperty(default="")
    effort = db.IntegerProperty(default=0)
    notes = db.TextProperty(default="")

    @classmethod
    def new_dbride(cls, bike, rider, route, **kwds):
        def txn():
            dbride = cls(parent=cls.db_parent, bike=bike, rider=rider, route=route, **kwds)
            dbride.put()
            return dbride

        return db.run_in_transaction(txn)


class DBComment(BaseModel, DBLocationModel, search.SearchableModel):
    @classmethod
    def SearchableProperties(cls):
        return [['review', 'category', 'city', 'state', 'country',]]
    route = db.ReferenceProperty(DBRoute)
    ride = db.ReferenceProperty(DBRide)
    team = db.ReferenceProperty(DBTeam)
    rider = db.ReferenceProperty(DBUser, collection_name="comments_to_me")
    reviewer = db.ReferenceProperty(DBUser, collection_name="my_comments", required=True)
    review = db.StringProperty(required=True, multiline=True)
    posted_on = db.DateTimeProperty(auto_now_add=True)

    photourl = db.StringProperty(default="")
    category = db.StringProperty(default=CommentCategory.text, choices=COMMENT_CATEGORIES)

    @classmethod
    def create(cls, reviewer, review, location=None, **kwds):
        def txn():
            comment = cls(parent=cls.db_parent, reviewer=reviewer, review=review, **kwds)
            if location is not None:
                comment.location = location
            comment.put()
            return comment

        return db.run_in_transaction(txn)

    @property
    def name(self):
        def truncate_chinese_words(s, num):
            from django.utils.text import truncate_words

            length = int(num)
            try:
                words = s.decode('ascii')
                return truncate_words(s, length)
            except:
                pass
            words = s
            if len(words) > length:
                words = words[:length]
                if not words[-1].endswith('...'):
                    words += ('...')
            return words

        return truncate_chinese_words(self.review, 35)


class DBActivity(BaseModel):
    rider = db.ReferenceProperty(DBUser, collection_name="my_activities")
    team = db.ReferenceProperty(DBTeam, collection_name="team_activities")
    create_datetime = db.DateTimeProperty(auto_now_add=True)
    activity_type = db.StringProperty(required=True, choices=ACTIVITY_TYPE)
    object_id_or_name = db.StringProperty(required=True)

    @classmethod
    def create_rider_activity(cls, rider, target, activity_type):
        def txn():
            activity = cls(parent=cls.db_parent, activity_type=activity_type,
                rider=rider, object_id_or_name=str(target.id_or_name))
            activity.put()
            return activity

        return db.run_in_transaction(txn)

    @classmethod
    def create_team_activity(cls, team, target, activity_type):
        def txn():
            activity = cls(parent=cls.db_parent, activity_type=activity_type,
                team=team, object_id_or_name=str(target.id_or_name))
            activity.put()
            return activity

        return db.run_in_transaction(txn)


class DBStatistics(BaseModel):
    #rider = db.ReferenceProperty(DBUser) #rider
    #route = db.ReferenceProperty(DBRoute) # stats for route
    #team = db.ReferenceProperty(DBTeam)  # stats for team

    riders_count = db.IntegerProperty(default=0)  #site, team

    teams_count = db.IntegerProperty(default=0)   #site, rider
    routes_count = db.IntegerProperty(default=0)  #site, rider
    routes_total_distance = db.FloatProperty(default=0.0)  #site, rider
    rides_count = db.IntegerProperty(default=0)   #site, rider, route
    rides_total_distance = db.FloatProperty(default=0.0)   #site, rider
    bikes_count = db.IntegerProperty(default=0)   #site, rider

    friends_count = db.IntegerProperty(default=0) #rider
    hours_on_bike = db.FloatProperty(default=0.0) #rider, in hours

    bikes_count = db.IntegerProperty(default=0)   #site, rider

    fav_count = db.IntegerProperty(default=0)  #route, rider

    comment_count = db.IntegerProperty(default=0) #route, rider

    point = db.IntegerProperty(default=0) #rider, team

    @classmethod
    def get_statistics(cls):
        return cls.get_or_insert("default_statistics", parent=cls.db_parent)

    @classmethod
    def get_default(cls):
        return cls.get_by_key_name("default_statistics")

    @classmethod
    def get_by_rider(cls, rider):
        key_name = "dbstats_rider:%s" % (rider.uid)
        entity = cls.get_by_key_name(key_name, parent=cls.db_parent)
        if entity is None:
            entity = cls(key_name=key_name, parent=cls.db_parent)
            entity.put()
        return entity

    @classmethod
    def get_by_route(cls, route):
        key_name = "dbstats_route:%s" % (route.id)
        entity = cls.get_by_key_name(key_name, parent=cls.db_parent)
        if entity is None:
            entity = cls(key_name=key_name, parent=cls.db_parent)
            entity.put()
        return entity

    @classmethod
    def get_by_bike(cls, bike):
        key_name = "dbstats_bike:%s" % (bike.id)
        entity = cls.get_by_key_name(key_name, parent=cls.db_parent)
        if entity is None:
            entity = cls(key_name=key_name, parent=cls.db_parent)
            entity.put()
        return entity

    @classmethod
    def get_by_team(cls, team):
        key_name = "dbstats_team:%s" % (team.id_or_name)
        entity = cls.get_by_key_name(key_name, parent=cls.db_parent)
        if entity is None:
            entity = cls(key_name=key_name, parent=cls.db_parent)
            entity.put()
        return entity

    @classmethod
    def add_route(cls, rider, dbroute):
        def txn():
            statistics = cls.get_default()
            statistics.routes_count += 1
            statistics.routes_total_distance += dbroute.distance
            statistics.save()

            rider_stats = cls.get_by_rider(rider)
            rider_stats.routes_count += 1
            rider_stats.point += ACTIVITY_POINT.get(ActivityType.CreateRoute, 0)
            rider_stats.routes_total_distance += dbroute.distance
            rider_stats.save()

        db.run_in_transaction(txn)

    @classmethod
    def add_fav(cls, rider, dbroute):
        def txn():
            rider_stats = cls.get_by_rider(rider)
            rider_stats.fav_count += 1
            rider_stats.point += ACTIVITY_POINT.get(ActivityType.AddFavorite, 0)
            rider_stats.save()

            route_stats = cls.get_by_route(dbroute)
            route_stats.fav_count += 1
            route_stats.save()

        db.run_in_transaction(txn)

    @classmethod
    def remove_fav(cls, rider, dbroute):
        def txn():
            rider_stats = cls.get_by_rider(rider)
            if rider_stats.fav_count >= 1:
                rider_stats.fav_count -= 1
                rider_stats.point += ACTIVITY_POINT.get(ActivityType.RemoveFavorite, 0)
                rider_stats.save()

            route_stats = cls.get_by_route(dbroute)
            if route_stats.fav_count >= 1:
                route_stats.fav_count -= 1
                route_stats.save()

        db.run_in_transaction(txn)

    @classmethod
    def add_ride(cls, rider, dbride):
        def txn():
            statistics = cls.get_default()
            statistics.rides_count += 1
            statistics.rides_total_distance += dbride.route.distance
            statistics.save()

            rider_stats = cls.get_by_rider(rider)
            rider_stats.rides_count += 1
            rider_stats.point += ACTIVITY_POINT.get(ActivityType.AddRide, 0)
            rider_stats.rides_total_distance += dbride.route.distance
            rider_stats.hours_on_bike += dbride.duration
            rider_stats.save()

            route_stats = cls.get_by_route(dbride.route)
            route_stats.rides_count += 1
            route_stats.save()

            bike_stats = cls.get_by_bike(dbride.bike)
            bike_stats.rides_total_distance += dbride.route.distance
            bike_stats.hours_on_bike += dbride.duration
            bike_stats.rides_count += 1
            bike_stats.save()

        db.run_in_transaction(txn)

    @classmethod
    def add_rider(cls, dbuser):
        def txn():
            statistics = cls.get_default()
            statistics.riders_count += 1
            statistics.save()

            rider_stats = cls.get_by_rider(dbuser)

        db.run_in_transaction(txn)

    @classmethod
    def add_team(cls, rider, dbteam):
        def txn():
            statistics = cls.get_default()
            statistics.teams_count += 1
            statistics.save()

            rider_stats = cls.get_by_rider(rider)
            rider_stats.teams_count += 1
            rider_stats.point += ACTIVITY_POINT.get(ActivityType.AddTeam, 0)
            rider_stats.save()

            team_stats = cls.get_by_team(dbteam)

        db.run_in_transaction(txn)

    @classmethod
    def dismiss_team(cls, rider, dbteam):
        def txn():
            statistics = cls.get_default()
            if statistics.teams_count >= 1:
                statistics.teams_count -= 1
                statistics.save()

            rider_stats = cls.get_by_rider(rider)
            rider_stats.teams_count -= 1
            rider_stats.point += ACTIVITY_POINT.get(ActivityType.DismissTeam, 0)
            rider_stats.save()

        db.run_in_transaction(txn)

    @classmethod
    def add_bike(cls, rider, dbbike=None):
        def txn():
            statistics = cls.get_default()
            statistics.bikes_count += 1
            statistics.save()

            rider_stats = cls.get_by_rider(rider)
            rider_stats.bikes_count += 1
            rider_stats.point += ACTIVITY_POINT.get(ActivityType.AddBike, 0)
            rider_stats.save()

        db.run_in_transaction(txn)

    @classmethod
    def remove_bike(cls, rider, dbbike=None):
        def txn():
            statistics = cls.get_default()
            if statistics.bikes_count >= 1:
                statistics.bikes_count -= 1
                statistics.save()

            rider_stats = cls.get_by_rider(rider)
            if rider_stats.bikes_count >= 1:
                rider_stats.bikes_count -= 1
                rider_stats.point += ACTIVITY_POINT.get(ActivityType.RemoveBike, 0)
                rider_stats.save()

        db.run_in_transaction(txn)

    @classmethod
    def add_friend(cls, rider, friend):
        def txn():
            rider_stats = cls.get_by_rider(rider)
            rider_stats.friends_count += 1
            rider_stats.point += ACTIVITY_POINT.get(ActivityType.AddFriend, 0)
            rider_stats.save()

            friend_stats = cls.get_by_rider(friend)
            friend_stats.friends_count += 1
            friend_stats.point += ACTIVITY_POINT.get(ActivityType.AddFriend, 0)
            friend_stats.save()

        db.run_in_transaction(txn)

    @classmethod
    def remove_friend(cls, rider, friend):
        def txn():
            rider_stats = cls.get_by_rider(rider)
            if rider_stats.friends_count >= 1:
                rider_stats.friends_count -= 1
                rider_stats.point += ACTIVITY_POINT.get(ActivityType.DeFriend, 0)
                rider_stats.save()

            friend_stats = cls.get_by_rider(friend)
            if friend_stats.friends_count >= 1:
                friend_stats.friends_count -= 1
                friend_stats.point += ACTIVITY_POINT.get(ActivityType.DeFriend, 0)
                friend_stats.save()

        db.run_in_transaction(txn)

    @classmethod
    def join_team(cls, rider, dbteam):
        def txn():
            rider_stats = cls.get_by_rider(rider)
            rider_stats.teams_count += 1
            rider_stats.point += ACTIVITY_POINT.get(ActivityType.JoinTeam, 0)
            rider_stats.save()

            team_stats = cls.get_by_team(dbteam)
            team_stats.riders_count += 1
            team_stats.save()

        db.run_in_transaction(txn)

    @classmethod
    def quit_team(cls, rider, dbteam):
        def txn():
            rider_stats = cls.get_by_rider(rider)
            if rider_stats.teams_count >= 1:
                rider_stats.teams_count -= 1
                rider_stats.point += ACTIVITY_POINT.get(ActivityType.QuitTeam, 0)
                rider_stats.save()

            team_stats = cls.get_by_team(dbteam)
            team_stats.riders_count -= 1
            team_stats.save()

        db.run_in_transaction(txn)

    @classmethod
    def add_comment(cls, rider, dbcomment):
        def txn():
            rider_stats = cls.get_by_rider(rider)
            rider_stats.comment_count += 1
            rider_stats.point += ACTIVITY_POINT.get(ActivityType.AddComment, 0)
            rider_stats.save()

            route_stats = cls.get_by_route(dbcomment.route)
            route_stats.comment_count += 1
            route_stats.save()

        db.run_in_transaction(txn)


DBStats = DBStatistics.get_statistics()

def get_statistics():
    return DBStatistics.get_statistics()


def main():
    pass

if __name__ == '__main__':
    main()
